﻿//     Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;
using Gateway.Console.Services;
using Gateway.Console.Services.Azure;
using Microsoft.Gateway.Configuration;
using Microsoft.Gateway.Utils;

namespace Gateway.Console.Models
{
    public class RoleApiController : ApiController
    {
        static readonly Version NullVersion = new Version();

        private ConsoleContext db = new ConsoleContext();

        // GET api/RoleApi
        public async Task<GatewayConfigurationIndex> Get()
        {
            Version apiVersion = GetApiVersion();
            await UpdateRequestorInfoAsync();
            var index = new GatewayConfigurationIndex();
            var s = await db.SecurityModels.FirstOrDefaultAsync();
            if (s != null && s.EnableAuthenticationValue)
            {
                index.ApplicationName = s.ApplicationName;
                index.Directory = s.Directory;
            }
            index.Roles = await db.RoleModels
                .ToDictionaryAsync(role => role.IsDefaultRole ? Role.ROLE_NAME_DEFAULT : role.RoleName,
                              role => this.Url.Link("DefaultApi", GetRequestPathAndQuery(role.Id, apiVersion)));

            return index;
        }

        IDictionary<string, object> GetRequestPathAndQuery(int roleId, Version apiVersion)
        {
            var retval = new Dictionary<string, object> {
                { "id", roleId }
            };
            if (apiVersion != NullVersion)
            {
                retval[ApiUriBuilder.QueryParamApiVersion] = apiVersion.GatewayFormat();
            }
            return retval;
        }

        // GET api/RoleApi/id
        public async Task<Role> GetRoleModel(int id)
        {
            Version apiVersion = GetApiVersion();
            var r = await db.RoleModels.FindAsync(id);
            var role = new Role();
            role.Segment = r.RoleName;
            if (r.Flights.Any(flight => flight.IsNonWeightedFlight))
            {
                role.Flights = r.Flights
                    .OrderByDescending(flight => flight.AddressList.Any() ? 3 :
                        flight.UserList.Any() ? 2 :
                        flight.TenantList.Any() ? 1 :
                        0)
                    .Select(flight => new Flight
                    {
                        Name = flight.Id.ToString(),
                        SourceIP = flight.AddressList.ToList(),
                        User = flight.UserList.ToList(),
                        Tenant = flight.TenantList.ToList(),
                        Target = Enumerable.Repeat(new TargetRoute(flight.Redirect, flight.Weight), 1).ToList(),
                    })
                    .ToList();
            }
            else
            {
                role.Target = r.Flights
                    .Select(flight => new TargetRoute(flight.Redirect, flight.Weight))
                    .ToList();
            }
            role.RequireAuthenticationBelow = r.Restrictions
                .Select(restriction => restriction.RestrictBelow)
                .ToList();
            return role;
        }

        async Task UpdateRequestorInfoAsync()
        {
            try
            {
                string requestor;
                Version requestorVersion;
                GetRequestorInfo(out requestor, out requestorVersion);
                if (requestorVersion != NullVersion)
                {
                    string serviceName = String.Empty;
                    // Try to match the requestor (by name or IP) to a deployed service
                    if (!String.IsNullOrWhiteSpace(requestor))
                    {
                        var domainParts = requestor.Split('.');
                        if (domainParts.Length > 2)
                        {
                            string hostDomain = String.Join(".", domainParts.Skip(domainParts.Length - 2));
                            if (hostDomain.Equals(AzureEnvironment.AzureCloudServiceUriSuffix, StringComparison.OrdinalIgnoreCase))
                            {
                                serviceName = String.Join(".", domainParts.Take(domainParts.Length - 2));
                            }
                        }
                    }
                    if (String.IsNullOrWhiteSpace(serviceName))
                    {
                        // Use the IP to match against known deployments
                        var ctx = this.Request.Properties["MS_HttpContext"] as HttpContextWrapper;
                        if (ctx != null)
                        {
                            string requestorIp = ctx.Request.UserHostAddress;
                            if (!String.IsNullOrWhiteSpace(requestorIp))
                            {
                                var deployments = await db.Deployments
                                    .Where(deployment => !String.IsNullOrEmpty(deployment.RefreshToken))
                                    .ToListAsync();
                                // Is there really no async Where()?
                                serviceName = (await Task.WhenAll(deployments
                                    .Select(async deployment => new
                                    {
                                        Deployment = deployment,
                                        IPMatch = await IsDeploymentMatchAddress(deployment, requestorIp),
                                    })))
                                    .Where(deployment => deployment.IPMatch)
                                    .Select(deployment => deployment.Deployment.ServiceName)
                                    .FirstOrDefault();
                            }
                        }
                    }
                    if (!String.IsNullOrWhiteSpace(serviceName))
                    {
                        var requestorDeployment = await db.Deployments
                            .FirstOrDefaultAsync(deployment => deployment.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
                        if (requestorDeployment != null)
                        {
                            bool updateVersion = true;
                            Version deploymentVersion;
                            if (VersionUtils.TryParseVersion(requestorDeployment.DeployedVersion, out deploymentVersion))
                            {
                                updateVersion = deploymentVersion < requestorVersion;
                            }
                            if (updateVersion)
                            {
                                requestorDeployment.DeployedVersion = requestorVersion.GatewayFormat();
                                db.Entry(requestorDeployment).State = EntityState.Modified;
                                await db.SaveChangesAsync();
                            }
                        }
                    }
                }
            }
            catch 
            {
                // Ok to fail silently here as this is not a critical operation
            }
        }

        static async Task<bool> IsDeploymentMatchAddress(Deployment deployment, string requestorIp)
        {
            var accessToken = OAuth2AuthenticationExtensions.GetAccessTokenFromRefreshToken(deployment.RefreshToken);
            using (var deploymentManager = new DeploymentManager(accessToken, deployment.SubscriptionSelection))
            {
                return String.Equals(requestorIp, await deploymentManager.GetServiceVIPAddressAsync(deployment.ServiceName), StringComparison.OrdinalIgnoreCase);
            }
        }

        Version GetApiVersion()
        {
            string version = GetRequestItem(ApiUriBuilder.QueryParamApiVersion);
            if (!String.IsNullOrWhiteSpace(version))
            {
                try
                {
                    return VersionUtils.ParseVersion(version);
                }
                catch
                {
                }
            }
            return NullVersion;
        }

        bool GetRequestorInfo(out string requestor, out Version currentVersion)
        {
            // Support determining service name from both explicit query param (legacy) AND reverse DNS lookup
            requestor = GetRequestItem(ApiUriBuilder.QueryParamRequestor);
            if (String.IsNullOrWhiteSpace(requestor))
            {
                requestor = ((HttpContextWrapper)this.Request.Properties["MS_HttpContext"]).Request.UserHostName;
            }
            string version = GetRequestItem(ApiUriBuilder.QueryParamCurrentVersion);
            if (!String.IsNullOrWhiteSpace(version))
            {
                try
                {
                    currentVersion = VersionUtils.ParseVersion(version);
                }
                catch
                {
                    currentVersion = NullVersion;
                }
            }
            else
            {
                currentVersion = NullVersion;
            }
            return true;
        }

        ILookup<string, string> _requestItems;
        ILookup<string, string> GetRequestItems()
        {
            if (_requestItems == null)
            {
                _requestItems = this.Request.Headers
                    .SelectMany(header => header.Value.Select(headerValue => new KeyValuePair<string, string>(header.Key, headerValue)))
                    .Concat(this.Request.GetQueryNameValuePairs())
                    .ToLookup(header => header.Key, header => header.Value, StringComparer.OrdinalIgnoreCase);
            }
            return _requestItems;
        }

        string GetRequestItem(string itemName)
        {
            return GetRequestItems()[itemName].FirstOrDefault();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}